package domain;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Joachim
 */
public class Client {

    // Singleton
    private static Client instance;
    // Server settings & connection
    private static InetAddress host;
    private static final int PORT = 1337;
    private static final int BUFFERSIZE = 1024;
    private Socket link;
    private DataInputStream socketIn;
    private DataOutputStream socketOut;
    private String email;
    private String rootDirectory;
    private DirectoryListener listener;
    private boolean synchronizing;
    private SettingsLoader settings;
    private File dir;
    ExecutorService exService;

    /*
     ** GENERAL
     */
    public static Client getInstance() {
        if (instance == null) {
            instance = new Client();
        }
        return instance;
    }

    private Client() {
        try {
            host = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {
            System.out.println("Host id not found!");
            System.exit(1);
        }

        listener = new DirectoryListener(this);
        listener.start();
        
        synchronizing = false;
        
        this.settings = new SettingsLoader();
        //exService = Executors.newFixedThreadPool(2);
        //exService.submit(new DirectoryListener(this));
        //exService.submit(new ServerListener(bufferedReader, this));
    }

    public void setRootDirectory(String rootDirectory) throws InvalidPathException, IOException {
        File file = new File(rootDirectory);

        if (!file.exists()) {
            throw new InvalidPathException(rootDirectory, "Invalid directory path!");
        }
        if (!file.isDirectory()) {
            throw new InvalidPathException(rootDirectory, "Not a directory!");
        }
        
        this.rootDirectory = rootDirectory;
        
        listener.setDirectory(rootDirectory);
        settings.setRootDirectory(rootDirectory);
        settings.saveSettings();
    }

    public String getRootDirectory() {
        return settings.getRootDirectory();
    }
    
    public String getEmail() {
        return settings.getEmail();
    }
    
    public void setDirectoryListener(boolean enabled)
    {
        listener.setActivated(enabled);
    }

    public boolean isSynchronizing()
    {
        return synchronizing;
    }

    /*
     ** INITIALIZATION
     */
    private void initConnection() throws ConnectException {
        try {
            link = new Socket(host, PORT);
            socketIn = new DataInputStream(link.getInputStream());
            socketOut = new DataOutputStream(link.getOutputStream());
        } catch (IOException e) {
            throw new ConnectException("Connection with server failed");
        }
    }

    public void closeConnection() {
        try {
            socketOut.writeUTF("closeConnection");
            link.close();
        } catch (IOException ex) {
            // Exception -> connection is already closed so do nothing
        }
    }

    /*
     ** ACCOUNT
     */
    public boolean logIn(String email, String password) throws IllegalArgumentException, ConnectException {
        if (email.isEmpty() || password.isEmpty()) {
            throw new IllegalArgumentException("All fields are required");
        } else if (!Utils.isValidEmail(email)) {
            throw new IllegalArgumentException("Email is not valid");
        }
        initConnection(); // Open connection with server
        Boolean loginSuccess = false;
        try {
            socketOut.writeUTF("login");
            socketOut.writeUTF(email);
            socketOut.writeUTF(password);
            loginSuccess = socketIn.readBoolean();
        } catch (IOException e) {
            throw new ConnectException("Connection with server failed");
        }
        if (loginSuccess) {
            this.email = email;
            settings.setEmail(email);
            settings.saveSettings();
        }
        //closeConnection();
        return loginSuccess;
    }

    public boolean register(String email, String password, String repeatPassword) throws IllegalArgumentException, ConnectException {
        if (email.isEmpty() || password.isEmpty() || repeatPassword.isEmpty()) {
            throw new IllegalArgumentException("All fields are required");
        } else if (!Utils.isValidEmail(email)) {
            throw new IllegalArgumentException("Email is not valid");
        } else if (!password.equals(repeatPassword)) {
            throw new IllegalArgumentException("Passwords don't match");
        }
        initConnection(); // Open connection with server
        Boolean registerSuccess = false;
        try {
            socketOut.writeUTF("register");
            socketOut.writeUTF(email);
            socketOut.writeUTF(password);
            registerSuccess = socketIn.readBoolean();
        } catch (IOException e) {
            throw new ConnectException("Connection with server failed");
        }
        closeConnection();
        return registerSuccess;
    }

    public boolean resetPassword(String email) throws IllegalArgumentException, ConnectException {
        if (!Utils.isValidEmail(email)) {
            throw new IllegalArgumentException("Email is not valid");
        }
        initConnection(); // Open connection with server
        Boolean resetPasswordSuccess = false;
        try {
            socketOut.writeUTF("resetPassword");
            socketOut.writeUTF(email);
            resetPasswordSuccess = socketIn.readBoolean();
        } catch (IOException e) {
            throw new ConnectException("Connection with server failed");
        }
        //closeConnection();
        return resetPasswordSuccess;
    }

    /*
     ** CLIENT ACTIONS
     */
    public void sendFile(String relatiefPath) {
        try {
            File file = new File(rootDirectory, relatiefPath);
            int fileSize = (int) file.length();
            
            socketOut.writeUTF("acceptFile");
            socketOut.writeUTF(dir.toURI().relativize(file.toURI()).getPath()); // Send file name
            socketOut.writeInt(fileSize); // Send file size
            
            int n;
            byte[] buffer = new byte[BUFFERSIZE];
            byte[] bufferS;
            DataInputStream fileIn = new DataInputStream(new FileInputStream(file));
            while ((n = fileIn.read(buffer)) > 0) {
                bufferS = new byte[n];
                System.arraycopy(buffer, 0, bufferS, 0, n);
                socketOut.write(bufferS, 0, n);
                //socketOut.flush();
            }
            fileIn.close();
            socketOut.flush();
        } catch (IOException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public boolean deleteServerFile(String path) {
        boolean deleteFileSuccess = false;

        try {
            socketOut.writeUTF("deleteFile");
            socketOut.writeUTF(path);

            if (socketIn.readUTF().equals("ok")) {
                deleteFileSuccess = true;
            }
        } catch (IOException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }

        return deleteFileSuccess;
    }

    /*
     ** SERVER ACTIONS
     */
    public void acceptFile() {
        try {
            String relatiefPath = socketIn.readUTF(); // Receive file name 
            int fileSize = socketIn.readInt(); // Receive file size
            
            File f = new File(rootDirectory, relatiefPath);
            File d = new File(f.getParent());
            d.mkdirs();
            DataOutputStream fileOut = new DataOutputStream(new FileOutputStream(f));
            
            int n, totalSize = 0;
            byte[] buffer = new byte[BUFFERSIZE];
            while (!(totalSize == fileSize)) {
                //System.gc();
                n = BUFFERSIZE;
                totalSize += n;
                if(totalSize > fileSize)
                {
                    n -= totalSize - fileSize;
                    totalSize = fileSize;
                }
                buffer = new byte[n];
                socketIn.readFully(buffer);
                fileOut.write(buffer);
                //fileOut.flush();
            }
            fileOut.flush();
            fileOut.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void deleteFile(String relatiefPath) {
        try {
            File file = new File(rootDirectory, relatiefPath);

            if (!file.exists()) {
                throw new IllegalArgumentException("Delete: no such file or directory: " + relatiefPath);
            }

            if (!file.canWrite()) {
                throw new IllegalArgumentException("Delete: write protected: " + relatiefPath);
            }

            Utils.deleteFileAndEmptyParents(file.toPath(), rootDirectory);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void deleteFile() {
        try {
            deleteFile(socketIn.readUTF());
        } catch (IOException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /*
     ** SYNCHRONIZATION
     */
    
    public void synchronizeFiles() throws ConnectException {
        synchronizing = true;
        
        File dirp = new File(getRootDirectory());
        dir = dirp;
        dir.mkdir();
        
        HashMap<String, File> fileMap = Utils.getFileMap(dir);
        
        System.out.println("-----");
        for (String str : fileMap.keySet()) {
            System.out.println(str);
        }
        System.out.println("-----");

        try {
            socketOut.writeUTF("synchronize");
            
            String type = socketIn.readUTF();
            String relatiefPath;
            long lastModified = 0;
            byte[] hash = new byte[20];
            
            while (!type.equals("end")) {
                relatiefPath = socketIn.readUTF();
                lastModified = socketIn.readLong();
                socketIn.readFully(hash);
                
                if (fileMap.containsKey(relatiefPath)) {
                    System.out.println("Client SHA1 " + relatiefPath + ": " + Utils.byteArrayToHex(Utils.calculateFileHash(fileMap.get(relatiefPath))));
                }
                System.out.println("Server SHA1 " + relatiefPath + ": " + Utils.byteArrayToHex(hash));
                
                if (fileMap.containsKey(relatiefPath)) {
                    if (Arrays.equals(Utils.calculateFileHash(fileMap.get(relatiefPath)), hash)) {
                        System.out.println(relatiefPath + " : synched");
                        socketOut.writeUTF("synched");
                    } else {
                        if (lastModified < Files.getLastModifiedTime(fileMap.get(relatiefPath).toPath()).toMillis()) {
                            System.out.println(relatiefPath + " : changed on client");
                            socketOut.writeUTF("changed_client");
                            sendFile(relatiefPath);
                        } else {
                            System.out.println(relatiefPath + " : changed on server");
                            socketOut.writeUTF("changed_server");
                            deleteFile(relatiefPath);
                            if(socketIn.readUTF().equals("acceptFile"))
                                acceptFile();
                        }

                    }
                    fileMap.remove(relatiefPath);
                } else {
                    System.out.println(relatiefPath + " : not found");
                    socketOut.writeUTF("notfound");
                    if(socketIn.readUTF().equals("acceptFile"))
                        acceptFile();
                    System.out.println(relatiefPath + " downloaded");
                }
                
                System.out.println("lezen");
                type = socketIn.readUTF();
                System.out.println("gelezen: " + type);
            }
            
            System.out.println("Nog over:");
            System.out.println("-----");
            for (String nieuw : fileMap.keySet()) {
                sendFile(nieuw);
            }
            System.out.println("-----");
            socketOut.writeUTF("end");
        } catch (IOException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        System.out.println("ENDAAA!!!!!!");
        
        synchronizing = false;
    }

}
